'use strict';

const BSON = require('../../register-bson');
const BSONError = BSON.BSONError;

/**
 * Binary Parser.
 * Jonas Raoni Soares Silva
 * http://jsfromhell.com/classes/binary-parser [v1.0]
 */
var chr = String.fromCharCode;

var maxBits = [];
for (var i = 0; i < 64; i++) {
  maxBits[i] = Math.pow(2, i);
}

function BinaryParser(bigEndian, allowExceptions) {
  if (!(this instanceof BinaryParser)) return new BinaryParser(bigEndian, allowExceptions);

  this.bigEndian = bigEndian;
  this.allowExceptions = allowExceptions;
}

BinaryParser.warn = function warn(msg) {
  if (this.allowExceptions) {
    throw new BSONError(msg);
  }

  return 1;
};

BinaryParser.decodeFloat = function decodeFloat(data, precisionBits, exponentBits) {
  var b = new this.Buffer(this.bigEndian, data);

  b.checkBuffer(precisionBits + exponentBits + 1);

  var bias = maxBits[exponentBits - 1] - 1,
    signal = b.readBits(precisionBits + exponentBits, 1),
    exponent = b.readBits(precisionBits, exponentBits),
    significand = 0,
    divisor = 2,
    curByte = b.buffer.length + (-precisionBits >> 3) - 1;

  do {
    for (
      var byteValue = b.buffer[++curByte], startBit = precisionBits % 8 || 8, mask = 1 << startBit;
      (mask >>= 1);
      byteValue & mask && (significand += 1 / divisor), divisor *= 2
    );
  } while ((precisionBits -= startBit));

  return exponent === (bias << 1) + 1
    ? significand
      ? NaN
      : signal
        ? -Infinity
        : +Infinity
    : (1 + signal * -2) *
        (exponent || significand
          ? !exponent
            ? Math.pow(2, -bias + 1) * significand
            : Math.pow(2, exponent - bias) * (1 + significand)
          : 0);
};

BinaryParser.decodeInt = function decodeInt(data, bits, signed, forceBigEndian) {
  var b = new this.Buffer(this.bigEndian || forceBigEndian, data),
    x = b.readBits(0, bits),
    max = maxBits[bits]; //max = Math.pow( 2, bits );

  return signed && x >= max / 2 ? x - max : x;
};

BinaryParser.encodeFloat = function encodeFloat(data, precisionBits, exponentBits) {
  var bias = maxBits[exponentBits - 1] - 1,
    minExp = -bias + 1,
    maxExp = bias,
    minUnnormExp = minExp - precisionBits,
    n = parseFloat(data),
    status = isNaN(n) || n === -Infinity || n === +Infinity ? n : 0,
    exp = 0,
    len = 2 * bias + 1 + precisionBits + 3,
    bin = new Array(len),
    signal = (n = status !== 0 ? 0 : n) < 0,
    intPart = Math.floor((n = Math.abs(n))),
    floatPart = n - intPart,
    lastBit,
    rounded,
    result,
    i,
    j;

  for (i = len; i; bin[--i] = 0);

  for (i = bias + 2; intPart && i; bin[--i] = intPart % 2, intPart = Math.floor(intPart / 2));

  for (i = bias + 1; floatPart > 0 && i; (bin[++i] = ((floatPart *= 2) >= 1) - 0) && --floatPart);

  for (i = -1; ++i < len && !bin[i]; );

  if (
    bin[
      (lastBit =
        precisionBits -
        1 +
        (i =
          (exp = bias + 1 - i) >= minExp && exp <= maxExp
            ? i + 1
            : bias + 1 - (exp = minExp - 1))) + 1
    ]
  ) {
    if (!(rounded = bin[lastBit])) {
      for (j = lastBit + 2; !rounded && j < len; rounded = bin[j++]);
    }

    for (j = lastBit + 1; rounded && --j >= 0; (bin[j] = !bin[j] - 0) && (rounded = 0));
  }

  for (i = i - 2 < 0 ? -1 : i - 3; ++i < len && !bin[i]; );

  if ((exp = bias + 1 - i) >= minExp && exp <= maxExp) {
    ++i;
  } else if (exp < minExp) {
    exp !== bias + 1 - len && exp < minUnnormExp && this.warn('encodeFloat::float underflow');
    i = bias + 1 - (exp = minExp - 1);
  }

  if (intPart || status !== 0) {
    this.warn(intPart ? 'encodeFloat::float overflow' : 'encodeFloat::' + status);
    exp = maxExp + 1;
    i = bias + 2;

    if (status === -Infinity) {
      signal = 1;
    } else if (isNaN(status)) {
      bin[i] = 1;
    }
  }

  for (
    n = Math.abs(exp + bias), j = exponentBits + 1, result = '';
    --j;
    result = (n % 2) + result, n = n >>= 1
  );

  var r;
  for (
    n = 0,
      j = 0,
      i = (result = (signal ? '1' : '0') + result + bin.slice(i, i + precisionBits).join(''))
        .length,
      r = [];
    i;
    j = (j + 1) % 8
  ) {
    n += (1 << j) * result.charAt(--i);
    if (j === 7) {
      r[r.length] = String.fromCharCode(n);
      n = 0;
    }
  }

  r[r.length] = n ? String.fromCharCode(n) : '';

  return (this.bigEndian ? r.reverse() : r).join('');
};

BinaryParser.encodeInt = function encodeInt(data, bits, signed, forceBigEndian) {
  var max = maxBits[bits];

  if (data >= max || data < -(max / 2)) {
    this.warn('encodeInt::overflow');
    data = 0;
  }

  if (data < 0) {
    data += max;
  }

  for (
    var r = [];
    data;
    r[r.length] = String.fromCharCode(data % 256), data = Math.floor(data / 256)
  );

  for (bits = -(-bits >> 3) - r.length; bits--; r[r.length] = '\0');

  return (this.bigEndian || forceBigEndian ? r.reverse() : r).join('');
};

BinaryParser.toSmall = function (data) {
  return this.decodeInt(data, 8, true);
};
BinaryParser.fromSmall = function (data) {
  return this.encodeInt(data, 8, true);
};
BinaryParser.toByte = function (data) {
  return this.decodeInt(data, 8, false);
};
BinaryParser.fromByte = function (data) {
  return this.encodeInt(data, 8, false);
};
BinaryParser.toShort = function (data) {
  return this.decodeInt(data, 16, true);
};
BinaryParser.fromShort = function (data) {
  return this.encodeInt(data, 16, true);
};
BinaryParser.toWord = function (data) {
  return this.decodeInt(data, 16, false);
};
BinaryParser.fromWord = function (data) {
  return this.encodeInt(data, 16, false);
};
BinaryParser.toInt = function (data) {
  return this.decodeInt(data, 32, true);
};
BinaryParser.fromInt = function (data) {
  return this.encodeInt(data, 32, true);
};
BinaryParser.toLong = function (data) {
  return this.decodeInt(data, 64, true);
};
BinaryParser.fromLong = function (data) {
  return this.encodeInt(data, 64, true);
};
BinaryParser.toDWord = function (data) {
  return this.decodeInt(data, 32, false);
};
BinaryParser.fromDWord = function (data) {
  return this.encodeInt(data, 32, false);
};
BinaryParser.toQWord = function (data) {
  return this.decodeInt(data, 64, true);
};
BinaryParser.fromQWord = function (data) {
  return this.encodeInt(data, 64, true);
};
BinaryParser.toFloat = function (data) {
  return this.decodeFloat(data, 23, 8);
};
BinaryParser.fromFloat = function (data) {
  return this.encodeFloat(data, 23, 8);
};
BinaryParser.toDouble = function (data) {
  return this.decodeFloat(data, 52, 11);
};
BinaryParser.fromDouble = function (data) {
  return this.encodeFloat(data, 52, 11);
};

// Factor out the encode so it can be shared by add_header and push_int32
BinaryParser.encode_int32 = function encode_int32(number, asArray) {
  var a, b, c, d, unsigned;
  unsigned = number < 0 ? number + 0x100000000 : number;
  a = Math.floor(unsigned / 0xffffff);
  unsigned &= 0xffffff;
  b = Math.floor(unsigned / 0xffff);
  unsigned &= 0xffff;
  c = Math.floor(unsigned / 0xff);
  unsigned &= 0xff;
  d = Math.floor(unsigned);
  return asArray ? [chr(a), chr(b), chr(c), chr(d)] : chr(a) + chr(b) + chr(c) + chr(d);
};

/**
 * UTF8 methods
 */

// Take a raw binary string and return a utf8 string
BinaryParser.decode_utf8 = function decode_utf8(binaryStr) {
  var len = binaryStr.length,
    decoded = '',
    i = 0,
    c = 0,
    c2 = 0,
    c3;

  while (i < len) {
    c = binaryStr.charCodeAt(i);
    if (c < 128) {
      decoded += String.fromCharCode(c);
      i++;
    } else if (c > 191 && c < 224) {
      c2 = binaryStr.charCodeAt(i + 1);
      decoded += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
      i += 2;
    } else {
      c2 = binaryStr.charCodeAt(i + 1);
      c3 = binaryStr.charCodeAt(i + 2);
      decoded += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
      i += 3;
    }
  }

  return decoded;
};

// Encode a cstring
BinaryParser.encode_cstring = function encode_cstring(s) {
  return unescape(encodeURIComponent(s)) + BinaryParser.fromByte(0);
};

// Take a utf8 string and return a binary string
BinaryParser.encode_utf8 = function encode_utf8(s) {
  var a = '',
    c;

  for (var n = 0, len = s.length; n < len; n++) {
    c = s.charCodeAt(n);

    if (c < 128) {
      a += String.fromCharCode(c);
    } else if (c > 127 && c < 2048) {
      a += String.fromCharCode((c >> 6) | 192);
      a += String.fromCharCode((c & 63) | 128);
    } else {
      a += String.fromCharCode((c >> 12) | 224);
      a += String.fromCharCode(((c >> 6) & 63) | 128);
      a += String.fromCharCode((c & 63) | 128);
    }
  }

  return a;
};

BinaryParser.hprint = function hprint(s) {
  var number;

  for (var i = 0, len = s.length; i < len; i++) {
    if (s.charCodeAt(i) < 32) {
      number =
        s.charCodeAt(i) <= 15 ? '0' + s.charCodeAt(i).toString(16) : s.charCodeAt(i).toString(16);
      process.stdout.write(number + ' ');
    } else {
      number =
        s.charCodeAt(i) <= 15 ? '0' + s.charCodeAt(i).toString(16) : s.charCodeAt(i).toString(16);
      process.stdout.write(number + ' ');
    }
  }

  process.stdout.write('\n\n');
};

BinaryParser.ilprint = function hprint(s) {
  var number;

  for (var i = 0, len = s.length; i < len; i++) {
    if (s.charCodeAt(i) < 32) {
      number =
        s.charCodeAt(i) <= 15 ? '0' + s.charCodeAt(i).toString(10) : s.charCodeAt(i).toString(10);

      require('util').debug(number + ' : ');
    } else {
      number =
        s.charCodeAt(i) <= 15 ? '0' + s.charCodeAt(i).toString(10) : s.charCodeAt(i).toString(10);
      require('util').debug(number + ' : ' + s.charAt(i));
    }
  }
};

BinaryParser.hlprint = function hprint(s) {
  var number;

  for (var i = 0, len = s.length; i < len; i++) {
    if (s.charCodeAt(i) < 32) {
      number =
        s.charCodeAt(i) <= 15 ? '0' + s.charCodeAt(i).toString(16) : s.charCodeAt(i).toString(16);
      require('util').debug(number + ' : ');
    } else {
      number =
        s.charCodeAt(i) <= 15 ? '0' + s.charCodeAt(i).toString(16) : s.charCodeAt(i).toString(16);
      require('util').debug(number + ' : ' + s.charAt(i));
    }
  }
};

/**
 * BinaryParser buffer constructor.
 */
function BinaryParserBuffer(bigEndian, buffer) {
  this.bigEndian = bigEndian || 0;
  this.buffer = [];
  this.setBuffer(buffer);
}

BinaryParserBuffer.prototype.setBuffer = function setBuffer(data) {
  var l, i, b;

  if (data) {
    i = l = data.length;
    b = this.buffer = new Array(l);
    for (; i; b[l - i] = data.charCodeAt(--i));
    this.bigEndian && b.reverse();
  }
};

BinaryParserBuffer.prototype.hasNeededBits = function hasNeededBits(neededBits) {
  return this.buffer.length >= -(-neededBits >> 3);
};

BinaryParserBuffer.prototype.checkBuffer = function checkBuffer(neededBits) {
  if (!this.hasNeededBits(neededBits)) {
    throw new BSONError('checkBuffer::missing bytes');
  }
};

BinaryParserBuffer.prototype.readBits = function readBits(start, length) {
  //shl fix: Henri Torgemane ~1996 (compressed by Jonas Raoni)

  function shl(a, b) {
    for (
      ;
      b--;
      a =
        ((a %= 0x7fffffff + 1) & 0x40000000) === 0x40000000
          ? a * 2
          : (a - 0x40000000) * 2 + 0x7fffffff + 1
    );
    return a;
  }

  if (start < 0 || length <= 0) {
    return 0;
  }

  this.checkBuffer(start + length);

  var offsetLeft,
    offsetRight = start % 8,
    curByte = this.buffer.length - (start >> 3) - 1,
    lastByte = this.buffer.length + (-(start + length) >> 3),
    diff = curByte - lastByte,
    sum =
      ((this.buffer[curByte] >> offsetRight) & ((1 << (diff ? 8 - offsetRight : length)) - 1)) +
      (diff && (offsetLeft = (start + length) % 8)
        ? (this.buffer[lastByte++] & ((1 << offsetLeft) - 1)) << ((diff-- << 3) - offsetRight)
        : 0);

  for (; diff; sum += shl(this.buffer[lastByte++], (diff-- << 3) - offsetRight));

  return sum;
};

/**
 * Expose.
 */
BinaryParser.Buffer = BinaryParserBuffer;

exports.BinaryParser = BinaryParser;
